package model;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Timer;
import java.util.logging.Level;

import javax.swing.SwingUtilities;

import model.contents.Content;
import model.feature.FeaturePlayer;
import net.HttpRequestFacade;
import platform.PlatformObjectFactory;
import threads.DownloadQueue;
import threads.PromotionThread;
import util.EmailSending;
import util.IModel;
import util.IView;
import util.LoggingUtils;
import util.PlayerUtilities;

public class Player implements IModel, Runnable, MenuBoxPlayer {

	private static Player _instance;

	private Map<Integer, Content> contents;
	private Map<Integer, Container> containers;
	private List<Playlist> playlists;

	private Queue<PlaylistRecord> queue;

	private List<PlaylistRecord> promotion;

	// the views listening to this model
	public List<IView> views = new ArrayList<IView>();
	protected final String className = this.getClass().getName();
	private boolean updated;

	public static synchronized Player getInstance() {
		if (_instance == null) {
			_instance = new Player();
		}
		return _instance;
	}

	private Player() {
		this.initialize();
	}

	public void resetPlayer() {
		this.contents = new HashMap<Integer, Content>();
		this.containers = new HashMap<Integer, Container>();
		this.playlists = new ArrayList<Playlist>();
		this.promotion = new ArrayList<PlaylistRecord>();
		this.queue = new LinkedList<PlaylistRecord>();
		this.notifyViewsWithAdditionalInformation("reset");
	}

	private void initialize() {
		this.resetPlayer();
		this.initializeOnLoad();
		this.loadPlayerData();
	}

	public void initializeOnLoad() {
		this.views = Collections.synchronizedList(new ArrayList<IView>());
		this.queue = new LinkedList<PlaylistRecord>();
	}

	private List<Playlist> loadPlaylist() {
		if (new File(Settings.PLAYLISTS_XML).exists()) {
			Object obj = PlayerUtilities.getObjectFromXMLFile(Settings.PLAYLISTS_XML);
			if (obj == null || !(obj instanceof ArrayList)) {
				File file = new File(Settings.PLAYLISTS_XML);
				file.delete();
				return new ArrayList<Playlist>();
			}
			List<Playlist> list = (ArrayList<Playlist>) obj;
			return list;
		} else {
			return new ArrayList<Playlist>();
		}
	}

	public void loadPlayerData() {

		this.playlists = this.loadPlaylist();

		if (new File(Settings.CONTENTS_XML).exists()) {
			Object obj = PlayerUtilities.getObjectFromXMLFile(Settings.CONTENTS_XML);
			if (obj != null) {
				this.contents = (Map<Integer, Content>) obj;
			} else {
				System.out.println("Failed to read " + Settings.CONTENTS_XML);
				File file = new File(Settings.CONTENTS_XML);
				file.delete();
			}
		}

		if (new File(Settings.CONTAINERS_XML).exists()) {
			Object obj = PlayerUtilities.getObjectFromXMLFile(Settings.CONTAINERS_XML);
			if (obj != null) {
				this.containers = (Map<Integer, Container>) obj;
			} else {
				System.out.println("Failed to read " + Settings.CONTAINERS_XML);
				File file = new File(Settings.CONTAINERS_XML);
				file.delete();
			}
		}

		updatePlayerFromCSVFiles();
		deleteCSVFiles();
		organizePlayerData();
		System.out.println(this.playlists);
	}

	private void updatePlayerFromCSVFiles() {
		List<Playlist> playlistFromCSVFile = Playlist.getPlaylistFromCSVFile(Settings.PLAYLISTS_CSV);
		
		List<Integer> jsonPlaylist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		
		for (Playlist p : playlistFromCSVFile) {
			Playlist removed = null;
			if (this.playlists.contains(p)) {
				removed = this.playlists.get(this.playlists.indexOf(p));
				this.playlists.remove(p);
			}

			// copy the playlist records from the old playlist to the new one
			if (removed != null) {
				for (PlaylistRecord r : removed.getRecords()) {
					p.addRecord(r);
				}
			}
			
			jsonPlaylist.add(p.getId());
			this.playlists.add(p);
		}
		
		PlayerUtilities.setUpdateStringJson("playlist",jsonPlaylist); // ***
		
		List<Integer> jsonContentlist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		
		for (Content c : Content.getContentListFromCSVFile(Settings.CONTENTS_CSV)) {
			this.contents.put(c.getId(), c);
			jsonContentlist.add(c.getId());
		}
		
		PlayerUtilities.setUpdateStringJson("contents",jsonContentlist); // ***
		List<Integer> jsonContainerlist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		
		for (Container c : Container.getContainerListFromCSVFile(Settings.CONTAINERS_CSV)) {
			this.containers.put(c.getId(), c);
			jsonContainerlist.add(c.getId());
		}
		
		PlayerUtilities.setUpdateStringJson("containers",jsonContainerlist); // ***
		List<Integer> jsonPlaylistRecord = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		
		for (PlaylistRecord r : PlaylistRecord.getPlaylistRecordListFromCSVFile(Settings.PLAYLIST_RECORDS_CSV)) {
			Playlist playlist = this.getPlaylist(r.getPlaylistID());
			if (playlist != null) {
				playlist.addRecord(r);
				
				jsonPlaylistRecord.add(r.getId());
			}
			
		}
		PlayerUtilities.setUpdateStringJson("playlistrecords",jsonPlaylistRecord); // ***
	}
	
	private void organizePlayerData() {
		Collections.sort(this.playlists);

		for (Container c : this.containers.values()) {
			c.initializeOnLoad();
		}

		for (Playlist p : this.playlists) {
			p.sortRecords();
			// set container and content for each record
			for (PlaylistRecord record : p.getRecords()) {
				record.setContainer(this.containers.get(record.getContainerID()));
				record.setContent(this.contents.get(record.getContentID()));
			}
		}
		this.notifyViews();
	}

	private void deleteCSVFiles() {
		if (new File(Settings.CONTAINERS_CSV).exists()) {
			new File(Settings.CONTAINERS_CSV).delete();
		}
		if (new File(Settings.CONTENTS_CSV).exists()) {
			new File(Settings.CONTENTS_CSV).delete();
		}
		if (new File(Settings.CONTAINERS_CSV).exists()) {
			new File(Settings.CONTAINERS_CSV).delete();
		}
		if (new File(Settings.PLAYLIST_RECORDS_CSV).exists()) {
			new File(Settings.PLAYLIST_RECORDS_CSV).delete();
		}
		if (new File(Settings.PLAYLISTS_CSV).exists()) {
			new File(Settings.PLAYLISTS_CSV).delete();
		}
	}

	public Playlist getPlaylist(int id) {
		for (Playlist p : this.playlists) {
			if (p.getId() == id) {
				return p;
			}
		}
		return null;
	}

	public boolean save() {
		String classMethod = className + ".save()";
		LoggingUtils.logStart(classMethod + " saving XML Files");

		if (!PlayerUtilities.saveObjectToXMLFile(Settings.CONTAINERS_XML, this.containers)) {
			System.out.format("Unable to save %s.\n", Settings.CONTAINERS_XML);
			LoggingUtils.log(Level.SEVERE, "Unable to save "+Settings.CONTAINERS_XML);
			return false;
		}

		if (!PlayerUtilities.saveObjectToXMLFile(Settings.CONTENTS_XML, this.contents)) {
			LoggingUtils.log(Level.SEVERE, "Unable to save "+Settings.CONTENTS_XML);
			System.out.format("Unable to save %s.\n", Settings.CONTENTS_XML);
			return false;
		}

		if (!PlayerUtilities.saveObjectToXMLFile(Settings.PLAYLISTS_XML, this.playlists)) {
			LoggingUtils.log(Level.SEVERE, "Unable to save "+Settings.PLAYLISTS_XML);
			System.out.format("Unable to save %s.\n", Settings.PLAYLISTS_XML);
			return false;
		}

		return true;

	}

	public boolean play() {
		PlaylistRecord record;

		FeaturePlayer.getInstance().play();

		synchronized (this.queue) {
			if (this.queue.isEmpty()) {
				this.generatePlayQueue();
			}
			if (this.queue.isEmpty()) {
				return false;
			} else {
				record = this.queue.poll();
			}
		}
		
		//System.out.println("Playing");
		//record.setClear(true);
		
		/* playing record by time every interval */
		if (record != null) {

			if(record.getPlaylistType() != null)
			{
				if(record.getPlaylistType().trim().equalsIgnoreCase("Time"))
				{
					if(!promotion.contains(record))
					{
						promotion.add(record);
					}
					return false;
				}
			}
			//System.out.println(record.getPlaylistType());
			return record.play();

		} else {
			return false;
		}
	}

	public void generatePlayQueue() {
		
		synchronized (this.queue) {
			this.queue.clear();
			PlayerUtilities.resetContainerContent();
			
			synchronized (this.playlists) {

				for (Playlist playlist : this.playlists) {
					List<PlaylistRecord> records = playlist.getRecords();
					
					synchronized (records) {
						for (PlaylistRecord r : records) {
							/*
							int ckf = checkFullScreen(r,records);
							if(ckf > 0)
							{
								//System.out.println(r.getContainer().getName()+"################# Setting clear area #######"+ckf+"############"+r.getContainer().getDepth());
								if(ckf == 1)
									r.setClear(true);
								else if(ckf == 2)
									r.setClear(false);
							}
							*/
							
							PlayerUtilities.setContainerContentValue(r.getContainer().getName());
							this.queue.offer(r);
						}
					}
					
				}

			}
			
			for (Map.Entry<String, Integer> item : PlayerUtilities.getContainerContent().entrySet()) {
				  String key = item.getKey();
				  int value = item.getValue();
				  System.out.println("###### Area = "+key+", Value = "+value);
				}
		}
	}
	
	public int checkFullScreen(PlaylistRecord r, List<PlaylistRecord> records)
	{
		try
		{
			/* if playlist record container is not full screen then return false */
			if(!r.getContainer().getName().trim().toUpperCase().equals("T") && !r.getContainer().getName().trim().toUpperCase().equals("FS"))
				return 0;

			//System.out.println(r.getContainer().getName()+"################# passting the braicade ######################"+r.getContainer().getDepth());

			/* getting the next record index */

			int currentIndex = records.indexOf(r);
			PlaylistRecord nextRecord = null;

			if(currentIndex + 1 >= records.size())
			{
				nextRecord = records.get(0);
			}
			else
			{
				nextRecord = records.get(currentIndex + 1);
			}
			
			if(nextRecord.getContainer().getName().trim().toUpperCase().equals(r.getContainer().getName().trim().toUpperCase()))
				return 2;

			if(r.getContainer().getDepth() > nextRecord.getContainer().getDepth())
			{
				return 1;
			}
		}
		catch(Exception e)
		{
			return 0;
		}
		return 0;
	}
	
	public boolean checkAreadepth(PlaylistRecord currentRecord, List<PlaylistRecord> records)
	{
		Collection<Container> collection =this.containers.values();
		
		/* Getting the maximum depth of element */
		
		List<Container> containerList = new ArrayList<Container>();
		containerList.addAll(collection);
		Container containerMax = Collections.max(containerList);
		
		
		/* If current container is not equal to the max container depth, then keep the record as it is */
		
		if(containerMax.getDepth() != currentRecord.getContainer().getDepth())
			return false;
		
		/* getting the next record */
		int currentIndex = records.indexOf(currentRecord);
		PlaylistRecord nextRecord = null;
		
		if(currentIndex + 1 >= records.size())
		{
			nextRecord = records.get(0);
		}
		else
		{
			nextRecord = records.get(currentIndex + 1);
		}
		
		//System.out.println(nextRecord.getContainer().getName()+"############ Next record depth #############"+nextRecord.getContainer().getDepth());
		
		/* if Current record has greater depth than next record than clear after pause time is true */
		
		if(currentRecord.getContainer().getDepth() > nextRecord.getContainer().getDepth())
		{
			return true;
		}
		
		return false;
	}
	
	public Queue<PlaylistRecord> getPlayQueue() {
		// do not need to synchronized this because returning a reference is
		// atomic in java
		return this.queue;
	}

	@Override
	public void addView(IView view) {
		synchronized (this.views) {
			if (!this.views.contains(view)) {
				this.views.add(view);
			}
		}
	}

	@Override
	public void removeView(IView view) {
		synchronized (this.views) {
			if (this.views.contains(view)) {
				this.views.remove(view);
			}
		}
	}

	@Override
	public void removeAllViews() {
		synchronized (this.views) {
			if (this.views == null) {
				this.views = new ArrayList<IView>();
			}

			try
			{
				this.views.clear();
			}
			catch(Exception e)
			{
				LoggingUtils.log(Level.SEVERE, e.getMessage(),e);
				System.out.println("Exception in clearing views from preference.java removeallviews()");
			}
		}
	}

	public void notifyViewsWithAdditionalInformation(Object info) {
		synchronized (this.views) {
			for (IView view : this.views) {
				System.out.println(view.getClass().getName());
				view.updateView(this, info);
			}

		}
	}

	@Override
	public void notifyViews() {
		/*
		synchronized (this.views) {
			for (IView view : this.views) {
				view.updateView(this, null);
			}

		}
		*/
		
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				synchronized (views) {
					for (IView view : views) {
						view.updateView(this, null);
					}
				}
			}
		});
	}

	public Collection<Content> getContents() {
		return this.contents.values();
	}

	public Collection<Container> getContainers() {
		return this.containers.values();
	}

	public List<Playlist> getPlaylists() {
		return this.playlists;
	}

	public List<PlaylistRecord> getPromotion() {
		return promotion;
	}

	public void setPromotion(List<PlaylistRecord> promotion) {
		this.promotion = promotion;
	}

	public void downloadContents() {
		
		Collection<Content> contents = this.getContents();
		// put in the queue
		synchronized (this.contents) {
			for (Content c : contents) {
				DownloadQueue.getInstance().addTask(c);
			}
		}
	}

	public synchronized void update() {
		// should synchronize each of the lists and maps
		this.setUpdated(true);
		try
		{
			this.queue.clear();
			System.out.println("UPDATE PLAYER SET");
		}
		catch(Exception e)
		{
			System.out.println("Exception in updating player"+e.getMessage());
			LoggingUtils.log(Level.SEVERE, e.getMessage(),e);
		}

		this.updatePlayerFromCSVFiles();
		this.deleteCSVFiles();
		this.organizePlayerData();
		this.generatePlayQueue();
		this.notifyViewsForUpdate();
		this.setUpdated(false);
		
	}

	public void setUpdated(boolean updated) {
		this.updated = updated;
	}

	public boolean isUpdated() {
		return this.updated;
	}

	public void deleteContainers(int[] containerIDs) {
		int length = containerIDs.length;
		List<Integer> jsonContainerlist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		synchronized (this.containers) {
			for (int i = 0; i < length; i++) {
				Container c = this.containers.remove(containerIDs[i]);
				if (c != null) {
					c.removeAllViews();
					jsonContainerlist.add(containerIDs[i]);
				}
			}
		}
		this.notifyViewsForUpdate();
		PlayerUtilities.setUpdateStringJson("DELETE_CONTAINER",jsonContainerlist);
	}

	public void deleteContents(int[] contentIDs) {
		int length = contentIDs.length;
		List<Integer> jsonContentlist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		synchronized (this.contents) {
			for (int i = 0; i < length; i++) {
				System.out.println(this.contents.get(contentIDs[i]).getPath());
				this.contents.get(contentIDs[i]).delete();
				this.contents.remove(contentIDs[i]);
				jsonContentlist.add(contentIDs[i]);
			}
		}
		this.notifyViewsForUpdate();
		PlayerUtilities.setUpdateStringJson("DELETE_CONTENT",jsonContentlist);
	}

	public void deletePlaylists(int[] playlistIDs) {
		Playlist plist;
		int deleteID;
		List<Integer> jsonPlaylist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		synchronized (this.playlists) {
			for (int i = 0; i < playlistIDs.length; i++) {
				deleteID = playlistIDs[i];
				for (int j = 0; j < this.playlists.size(); j++) {
					plist = this.playlists.get(j);
					if (plist.getId() == deleteID) {
						// found the playlist to be deleted.
						System.out.println("Deleting playlist " + plist.getId());
						this.playlists.remove(j);
						jsonPlaylist.add(deleteID);
						break;
					}
				}
			}
		}
		this.notifyViewsForUpdate();
		PlayerUtilities.setUpdateStringJson("DELETE_PLAYLIST",jsonPlaylist);
	}

	public void deletePlaylistRecords(int[] recordIDs) {
		List<Integer> jsonPlaylist = new ArrayList<Integer>(); // Prepearing json string to send to webtool for updates
		synchronized (this.playlists) {
			for (int i = 0; i < recordIDs.length; i++) {
				
				for (Playlist playlist : this.playlists) {

					if (playlist.removeRecord(recordIDs[i])) {
						// record is deleted, move to the next loop
						System.out.println("#############Remove from playlist as well######################");
						jsonPlaylist.add(recordIDs[i]);
					}
					
					System.out.println("##########################################");
					
				}
			}
		}
		this.notifyViewsForUpdate();
		System.out.println("FROM PLAYER "+jsonPlaylist.toString());
		PlayerUtilities.setUpdateStringJson("DELETE_RECORD",jsonPlaylist);
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (Playlist playlist : this.playlists) {
			sb.append(playlist.toString());
		}
		return sb.toString();
	}

	public int getNumberOfRecords() {
		int count = 0;
		synchronized (this.playlists) {
			for (Playlist p : this.playlists) {
				count += p.getRecords().size();
			}
		}
		return count;
	}

	private void notifyLinuxBrowserViews()
	{
		for (IView view : this.views) {
			view.updateView(this, null);
		}
	}

	private void notifyViewsForUpdate() {

		synchronized (this.views) {

			for (IView view : this.views) {
				view.updateView(this, "update");
			}

		}
	}

	@Override
	public void run() {
		final String classMethod = className + ".run";
		LoggingUtils.logStart(classMethod);

		try {
			while (true) 
			{
				if (this.isUpdated()) {
					Thread.yield();
				} else if (!this.play()) {
					// // System.out.println("NO RECORD TO PLAY");
					// try {
					// Thread.sleep(1000);
					// } catch (InterruptedException e) {
					// // TODO Auto-generated catch block
					// e.printStackTrace();
					// }
					//LoggingUtils.log(Level.SEVERE, "NO RECORD TO PLAY " + classMethod);
				}
				else
				{
				}

				//################# for promotion

				if(promotion != null)
				{
					///System.out.println("PRMOTION SIZE IS ############### "+promotion.size()+" ################");

					if(promotion.size()>0)
					{
						for(PlaylistRecord precord : promotion)
						{
							if(!PromotionThread.getPromotionList().contains(precord.getContentID()))
							{
								Timer timer = new Timer();
								timer.schedule(new PromotionThread(precord), 0, precord.getScheduleTime()*1000);
							}
							else
							{
								//System.out.println("PROMOTION THREAD ALREADY RUNNING FROM PLAYER "+precord.getContentID());
							}
						}
					}
				}

				/*if(promotion != null)
				{
					System.out.println("PRMOTION SIZE IS ############### "+promotion.size()+" ################");

					if(promotion.size()>0)
					{
						for(PlaylistRecord precord : promotion)
						{
							if(!PromotionThread.getPromotionList().contains(precord.getContentID()))
							{
								Timer timer = new Timer();

								if(precord.getContentID() == 103231)
								{
									timer.schedule(new PromotionThread(precord), 0, 10000);
								}
								else if(precord.getContentID() == 103226)
								{
									timer.schedule(new PromotionThread(precord), 0, 20000);
								}
								else if(precord.getContentID() == 103225)
								{
									timer.schedule(new PromotionThread(precord), 0, 30000);
								}
							}
							else
							{
								System.out.println("PROMOTION THREAD ALREADY RUNNING FROM PLAYER "+precord.getContentID());
							}
						}
					}
				}*/
			}

		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("restarting player since an exception occured");


			/* Sending exception to server */
			HttpRequestFacade.postPlayerException("Critical", classMethod+"\n"+EmailSending.FormatException(e),"Restart");

			/* Sending email to server, for exception occuring */

			String EmailBody = EmailSending.ProcessEmailBody("Exception occured in "+classMethod+"." +
					". restarting player.", e.getMessage(), EmailSending.FormatException(e));

			util.EmailSending.getInstance().send(Settings.TO_USER, "Restarting:Player:" + "BoxID=" + 
					Preference.getInstance().getBoxID(), EmailBody);

			/* Writing log to box for exception and detecting errors */

			LoggingUtils.log(Level.SEVERE, "Restarting player due to unexpected exception in " 
					+ classMethod,e);

			/* as critical error, player is getting restart */

			PlatformObjectFactory.getInstance().getCommandExecutor().restartPlayer();
		}
		catch (Error er) {
			// #Code Fix# for client crash and restart workaround. 				
			// Catch for Error is added to restart player whenever a severe error caused the
			// system to crash. Now the shutdown hook in Test should be able to determine 
			// that this is a SEVERE error shutdown and to act accordingly.
			// Note: setting quitPlayerShutdown might seem unnecessary, but no harm in doing so.

			/* Sending exception to server */
			HttpRequestFacade.postPlayerException("Critical", classMethod+"\n"+EmailSending.FormatException(er),"Restart");


			/* Writing log to box for exception and detecting errors */

			LoggingUtils.log(Level.SEVERE, "Restarting player due to unexpected exception in " 
					+ classMethod,er);

			/* Sending email to server, for exception occuring */

			String EmailBody = EmailSending.ProcessEmailBody("Exception occured in "+classMethod+"." +
					". restarting player.", er.getMessage(), EmailSending.FormatException(er));


			util.EmailSending.getInstance().send(Settings.TO_USER, "Restarting:Player:" + "BoxID=" + 
					Preference.getInstance().getBoxID(), EmailBody);

			/* as critical error, player is getting restart */
			PlatformObjectFactory.getInstance().getCommandExecutor().restartPlayer();
		}
	}

	public List<PlaylistRecord> getRecords() {
		List<PlaylistRecord> records = new ArrayList<PlaylistRecord>();
		synchronized(this.playlists) {
			for(Playlist playlist: this.playlists) {
				synchronized(playlist) {
					records.addAll(playlist.getRecords());
				}
			}
		}
		return records;
	}

	public Content getContent(int id) {
		return this.contents.get(new Integer(id));
	}

	public Container getContainer(int id) {
		return this.containers.get(new Integer(id));
	}
	
	public static void main(String[] args) {
		
			Object obj = PlayerUtilities.getObjectFromXMLFile(Settings.CONTAINERS_XML);
			if (obj != null) {
				Map<Integer, Container> con = (Map<Integer, Container>) obj;
			
			//Player pl = new Player();
			
			//if(pl.checkAreadepth(max, records, containers))
				
				/*
			Collection<Container> collection =	con.values();
			
			List<Container> ls = new ArrayList<Container>();
			ls.addAll(collection);
			Collections.sort(ls);
			Container max = Collections.max(ls);
			System.out.println(max.getDepth());
			
			Iterator<Container> it=ls.iterator();

	        while(it.hasNext())
	        {
	        	Container value=it.next();
	        	System.out.println(value.getDepth());
	        }
	        */
		}
	}
}
